﻿using Sidvall.Data.Entity.Linq;
using Sidvall.Serialization;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;

namespace Sidvall.Data.Entity
{
    public class DataManager
    {
        #region Public Members

        #region LogManager

        private Sidvall.Logging.LogManager _LogManager;
        public virtual Sidvall.Logging.LogManager LogManager
        {
            get
            {
                if (_LogManager != null)
                    return _LogManager;
                return Sidvall.SystemContext.Current.LogManager;
            }
            set
            {
                _LogManager = value;
            }
        }

        #endregion
        #region LogSaveChanges

        public virtual bool LogSaveChanges
        {
            get
            {
                return false;
            }
        }

        #endregion

        #region AddFilter

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        public System.Linq.IQueryable<T> AddFilter<T>(System.Linq.IQueryable<T> query, string fieldName, Sidvall.Data.ComparisonOperator comparisonOperator, object value)
        {
            var expression = GetFilterExpression<T>(comparisonOperator, value, fieldName);
            if (expression != null)
                query = query.Where(expression);
            return query;
        }

        #endregion
        #region ExecuteAsync

        public async Task ExecuteAsync(System.Data.Entity.DbContext context, SqlQuery query)
        {
            if (query == null)
                return;
            await context.Database.ExecuteSqlCommandAsync(query.Sql.ToString(), query.ParameterValues.ToArray()).ConfigureAwait(false);
        }
        public async Task ExecuteAsync(System.Data.Entity.DbContext context, SqlQueryCollection queries)
        {
            if (queries == null)
                return;
            if (queries.UseTransaction)
            {
                using (var tr = context.Database.BeginTransaction())
                {
                    foreach (var query in queries)
                        await context.Database.ExecuteSqlCommandAsync(query.Sql.ToString(), query.ParameterValues.ToArray()).ConfigureAwait(false);
                    tr.Commit();
                }
            }
            else
            {
                foreach (var query in queries)
                    await context.Database.ExecuteSqlCommandAsync(query.Sql.ToString(), query.ParameterValues.ToArray()).ConfigureAwait(false);
            }
        }

        #endregion
        #region GetEntityObjectAsync

        public async Task<T> GetEntityObjectAsync<T>(System.Linq.IQueryable<T> query)
        {
            return await query.FirstOrDefaultAsync().ConfigureAwait(false);
        }
        public async Task<T> GetEntityObjectAsync<T>(System.Linq.IQueryable<T> query, DataRowState dataRowState) where T : IDataRowContextProvider
        {
            var item = await query.FirstOrDefaultAsync().ConfigureAwait(false);
            if (item != null)
                item.SetDataRowContext(null, dataRowState);
            return item;
        }

        #endregion
        #region GetEntityObjectsAsync

        public async Task<List<T>> GetEntityObjectsAsync<T>(System.Linq.IQueryable<T> query)
        {
            var items = new List<T>();
            await query.ForEachAsync((item) =>
            {
                if (item != null)
                    items.Add(item);
            }).ConfigureAwait(false);
            return items;
        }
        public async Task<List<T>> GetEntityObjectsAsync<T>(System.Linq.IQueryable<T> query, DataRowState dataRowState) where T : IDataRowContextProvider
        {
            var items = new List<T>();
            await query.ForEachAsync((item) =>
            {
                if (item != null)
                {
                    item.SetDataRowContext(null, dataRowState);
                    items.Add(item);
                }
            }).ConfigureAwait(false);
            return items;
        }

        #endregion
        #region GetEntityObjectCountAsync

        public async Task<long> GetEntityObjectCountAsync<T>(System.Linq.IQueryable<T> query)
        {
            return await query.LongCountAsync().ConfigureAwait(false);
        }

        #endregion
        #region LoadEntityObjectsAsync

        public async Task LoadEntityObjectsAsync<T>(System.Linq.IQueryable<T> query, ICollection<T> source)
        {
            await query.ForEachAsync((item) =>
            {
                if (item != null)
                    source.Add(item);
            }).ConfigureAwait(false);
        }
        public async Task LoadEntityObjectsAsync<T>(System.Linq.IQueryable<T> query, ICollection<T> source, DataRowState dataRowState) where T : IDataRowContextProvider
        {
            await query.ForEachAsync((item) =>
            {
                if (item != null)
                {
                    item.SetDataRowContext(null, dataRowState);
                    source.Add(item);
                }
            }).ConfigureAwait(false);
        }

        #endregion
        #region LogAsync

        public async Task LogAsync(System.Exception ex)
        {
            if (this.LogManager != null)
                await this.LogManager.WriteAsync(ex, Sidvall.Logging.LogCategories.DataLayer);
        }
        public async Task LogAsync(Sidvall.Business.IDataPortalContext context)
        {
            if (context == null)
                return;
            if (this.LogManager != null)
            {
                if (!this.LogManager.ShouldLog(new string[] { Sidvall.Logging.LogCategories.DataLayer }, Logging.EventType.Information))
                    return;
                var title = context.Resource.Append(context.Action, ".");
                var message = Sidvall.Serialization.SerializerManager.Current.SerializeObject(context);
                await this.LogManager.WriteAsync(title, message, new string[] { Sidvall.Logging.LogCategories.DataLayer }, Logging.EventType.Information);
            }
        }

        #endregion
        #region PrepareFilter

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
        public System.Linq.IQueryable<T> PrepareFilter<T>(System.Linq.IQueryable<T> query, IFilterItem filter,
            System.Func<Sidvall.Data.IFilterItem, string> getFieldNameHandler)
        {
            var expression = GetFilterExpression<T>(filter, getFieldNameHandler);
            if (expression != null)
                query = query.Where(expression);
            return query;
        }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        public System.Linq.IQueryable<T> PrepareFilter<T>(System.Linq.IQueryable<T> query, Sidvall.Data.ComparisonOperator comparisonOperator, object value, string fieldName)
        {
            var expression = GetFilterExpression<T>(comparisonOperator, value, fieldName);
            if (expression != null)
                query = query.Where(expression);
            return query;
        }

        #endregion
        #region PrepareSort

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
        public System.Linq.IQueryable<T> PrepareSort<T>(System.Linq.IQueryable<T> query, System.Collections.Generic.IEnumerable<global::Sidvall.Data.SortItem> sortItems,
             System.Func<Sidvall.Data.SortItem, string> getFieldNameHandler)
        {
            string fieldName;

            if (sortItems == null)
                return query;
            if (!sortItems.Any())
                return query;

            var sb = new System.Text.StringBuilder();
            foreach (var sortItem in sortItems)
            {
                if (getFieldNameHandler != null)
                    fieldName = getFieldNameHandler(sortItem);
                else
                    fieldName = sortItem.FieldName;
                if (sb.Length > 0)
                    sb.Append(",");
                if (sortItem.Ascending)
                    sb.Append(fieldName);
                else
                    sb.Append(fieldName + " desc");
            }
            query = query.OrderBy(sb.ToString());

            return query;
        }

        #endregion
        #region SaveChangesAsync

        public async Task<int> SaveChangesAsync(System.Data.Entity.DbContext context)
        {
            if (context == null)
                throw new System.ArgumentNullException(nameof(context));

            try
            {
                if (this.LogSaveChanges)
                    context.Database.Log = WriteToLog;
                else
                    context.Database.Log = null;
                return await context.SaveChangesAsync().ConfigureAwait(false);
            }
            catch (System.Data.Entity.Infrastructure.DbUpdateConcurrencyException)
            {
                throw new Sidvall.Data.UpdateConcurrencyException();
            }
        }

        #endregion
        #region WriteToLog

        public async void WriteToLog(string message)
        {
            if (this.LogManager != null)
            {
                if (!this.LogManager.ShouldLog(new string[] { Sidvall.Logging.LogCategories.DataLayer }, Logging.EventType.Information))
                    return;
                await this.LogManager.WriteAsync("Entity Framework Log", message, Sidvall.Logging.LogCategories.DataLayer).ConfigureAwait(false);
            }
        }

        #endregion

        #endregion
        #region Private Members

        #region GetFilterExpression

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
        private System.Linq.Expressions.Expression<System.Func<TEntity, bool>> GetFilterExpression<TEntity>(IFilterItem filter,
            System.Func<Sidvall.Data.IFilterItem, string> getFieldNameHandler)
        {
            System.Linq.Expressions.Expression<System.Func<TEntity, bool>> expression, innerExpression;
            string fieldName;

            if (filter == null)
                return null;

            var subFilters = filter.GetSubFilters();
            if (subFilters == null)
            {
                if (getFieldNameHandler != null)
                    fieldName = getFieldNameHandler(filter);
                else
                    fieldName = filter.FieldName;
                if (filter.ValueIsArray)
                    return GetListFilterExpression<TEntity>(filter.ComparisonOperator, filter.Value, fieldName);
                else
                    return GetFilterExpression<TEntity>(filter.ComparisonOperator, filter.Value, fieldName);
            }
            else if (subFilters.Count == 0)
            {
                return null;
            }
            else if (subFilters.Count == 1)
            {
                return GetFilterExpression<TEntity>(subFilters.First(), getFieldNameHandler);
            }
            else
            {
                switch (filter.LogicalOperator)
                {
                    case LogicalOperator.And:
                        expression = null;
                        foreach (var item in subFilters)
                        {
                            innerExpression = GetFilterExpression<TEntity>(item, getFieldNameHandler);
                            if (innerExpression != null)
                            {
                                if (expression == null)
                                    expression = PredicateBuilder.True<TEntity>();
                                expression = expression.And(innerExpression);
                            }
                        }
                        break;
                    default:
                        expression = null;
                        foreach (var item in subFilters)
                        {
                            innerExpression = GetFilterExpression<TEntity>(item, getFieldNameHandler);
                            if (innerExpression != null)
                            {
                                if (expression == null)
                                    expression = PredicateBuilder.False<TEntity>();
                                expression = expression.Or(innerExpression);
                            }
                        }
                        break;
                }
                return expression;
            }
        }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
        private static System.Linq.Expressions.Expression<System.Func<TEntity, bool>> GetFilterExpression<TEntity>(Sidvall.Data.ComparisonOperator comparisonOperator, object value, string fieldName)
        {
            switch (comparisonOperator)
            {
                case ComparisonOperator.BitwiseEqual:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " & @0 == @1", value, value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " == null");
                case ComparisonOperator.BitwiseNotEqual:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " & @0 == 0", value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " == null");
                case ComparisonOperator.NotEqual:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " != @0", value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " != null");
                case ComparisonOperator.LessThan:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " < @0", value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " < null");
                case ComparisonOperator.LessThanOrEqual:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " <= @0", value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " <= null");
                case ComparisonOperator.GreaterThan:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " > @0", value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " > null");
                case ComparisonOperator.GreaterThanOrEqual:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " >= @0", value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " >= null");
                case ComparisonOperator.Like:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + ".Contains(@0)", value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + ".Contains(null)");
                case ComparisonOperator.NotLike:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>("!" + fieldName + ".Contains(@0)", value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>("!" + fieldName + ".Contains(null)");
                case ComparisonOperator.StartsWith:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + ".StartsWith(@0)", value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + ".StartsWith(null)");
                case ComparisonOperator.EndsWith:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + ".EndsWith(@0)", value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + ".EndsWith(null)");
                case ComparisonOperator.In:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + ".Contains(@0)", value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + ".Contains(null)");
                case ComparisonOperator.NotIn:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>("!" + fieldName + ".Contains(@0)", value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>("!" + fieldName + ".Contains(null)");
                default:
                    if (value != null)
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " == @0", value);
                    else
                        return Sidvall.Data.Entity.Linq.DynamicExpression.ParseLambda<TEntity, bool>(fieldName + " == null");
            }
        }

        #endregion
        #region GetListFilterExpression

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
        private static System.Linq.Expressions.Expression<System.Func<TEntity, bool>> GetListFilterExpression<TEntity>(Sidvall.Data.ComparisonOperator comparisonOperator, object value, string fieldName)
        {
            ParameterExpression parameter;
            MemberExpression memberAccess;
            Expression condition;
            System.Type type;
            System.Array array;

            if (string.IsNullOrWhiteSpace(fieldName))
                throw new System.ArgumentNullException("fieldName");

            array = value as System.Array;
            if ((array == null) || (array.Length == 0))
                return null;
            type = array.GetValue(0).GetType();
            memberAccess = null;
            parameter = Expression.Parameter(typeof(TEntity), "p");
            foreach (var property in fieldName.Split('.'))
                memberAccess = MemberExpression.Property(memberAccess ?? (parameter as Expression), property);
            if (System.Nullable.GetUnderlyingType(memberAccess.Type) != null)
                memberAccess = MemberExpression.Property(memberAccess, "Value");
            condition = Expression.Call(typeof(System.Linq.Enumerable), "Contains", new System.Type[] { type }, Expression.Constant(value), memberAccess);
            switch (comparisonOperator)
            {
                case ComparisonOperator.NotEqual:
                    return (System.Linq.Expressions.Expression<System.Func<TEntity, bool>>)Expression.Lambda(Expression.Not(condition), parameter);
                default:
                    return (System.Linq.Expressions.Expression<System.Func<TEntity, bool>>)Expression.Lambda(condition, parameter);
            }
        }

        #endregion

        #endregion
    }
}
